Measure Load
Volume Number: 4
Issue Number: 3
Column Tag: C Workshop
A Background Task for Measuring Load
By Peter Korn, Apple Computer
Peter Korn is a full time software quality assurance engineer for Apple
Computer, and a full time undergraduate student in Mathematics at UC Berkely, which
makes him a busy young man! In this article, he gives us a technique for measuring the
activity under multifinder by timing null events in a background task.
True™ Multitasking
I don’t have to tell you that multitasking is good. By not having to wait on the
computer to finish a task before you can go on to the next one you save time,
frustration, and creative energies. There’s nothing as frustrating to me as having an
idea and wanting to take it somewhere but not being able to do so because my computer
is busy printing, or downloading, or compiling (admittedly with LightSpeed C my wait
isn’t very long). But once we have the concept of multitasking, is multifinder True™
multitasking? Before I tackle that one, I want to give a brief history...
In the beginning there was the Macintosh (128K model). With it I could have a
Desk Accessory or three pop up whenever I had an idea that I didn’t want to interrupt
whatever else I was doing to work on. And, if I hadn’t foolishly started a print job or
an {up,down} loading session on my modem, I was fine. Some of the usefulness of
multitasking already!
When the 512K Mac came along there was more RAM to play with, and some
programs took advantage of this RAM to use the Vertical Retrace Manager for more than
just a cursor blinker and tick counter. With programs like SuperSpool, I could bring
up a DA even while my program was printing. And the DAs were getting bigger too.
There were spreadsheets, filers, outliners and word processors and terminal
programs (even one called Backdown, that would download for me in the background).
And while all of this took even more RAM, the Mac Plus was released shortly and life
was pretty ok.
Except that for a program to allow itself to be run ‘in the background’, it had to
go through a series of contortions, and deny itself use of the Memory Manager, and so
very few programs were written this way. Certainly Backdown, while nice, wasn’t a
full-blown terminal emulator. And still there was no way of checking spelling in the
background, or compiling, or recalculating a spreadsheet, or what-have-you. The
Vertical Retrace Manager just wasn’t cut out to be a scheduler.
Even with Switcher I still couldn’t do all these sorts of things at once; I could
only switch between them when they were idle. And this I had to do by hand (with the
exception of ThunderScan, but there’s no surprise there, as it shares authors with
Switcher).
Fig. 1Our little Load Average Window Measures time between null events
A True™ multitasking system should allow me to run True™ tasks, and have them
all going simultaneously (or close to simultaneously--you know what I mean). But an
important question is, must this be preemptive? As a user, do I really care how it’s
all done, so long as my download, spreadsheet recalculation, and mandelbrot plot all
continue whilst I’m busy writing a letter to grandma? In fact, if I’m typing my letter,
I don’t want the computer to be any slower responding to my keystrokes just because
it’s doing other things--I’m being creative here, and I don’t want to have to deal with a
sluggish interface that slows me down just because I have the computer doing other
things as well.
In wider circles, True™ multitasking is considered preemptive multitasking.
Each task (program, driver, what-have-you) is assigned some priority number (-10
to 10, say), and some scheduler program arbitrates which task gets how much CPU
time when. First it looks at all the programs of the highest priority level, and for all
of those that want time (some may not; perhaps their waiting for slow networking i/o,
or are finished computing their mandelbrot and haven’t been given further
instructions), it allocates that time evenly amongst them, interrupting each at some
small, regular interval (100 milliseconds, say), and giving time over to the next
needy task of equal priority level. When no tasks at that priority level want time, then
the scheduler looks at tasks of a lower priority level and parcels out time as before to
all of them, and so on. If the user doesn’t want his creativity disrupted, he merely sets
the priority of the task he is working on to something higher than all the other tasks.
And in between keystrokes (in the word processor, say), the other tasks will get time
to do what they need to do (ie: when the user is idle [for some fraction of a second]).
Apple, in it’s infinite wisdom, didn’t do it this way. The main reason was for
compatibility. Another was because of system architecture (while the Amiga can carry
it off without a hardware Memory Management Unit that ensures that whatever
memory address a program writes to isn’t taken by something else, certain developers
[including Apple] have too long thought of the Macintosh as belonging entirely to the
program currently running [allowing for those piddling desk accessories, maybe] to
pull this off). But is Apple’s way also True™ multitasking? Well, if it gets the job
done...
Multitasking the Macintosh way
Under the new operating system (System Disk 5.0, which contains System 4.2,
Finder 6.0, MultiFinder 1.0, Chooser and Control Panel 3.2, and [aha, so this is where
they got the number] LaserWriter Driver 5.0), we can run a program called
MultiFinder. MultiFinder, (like Switcher and Servant before them) allows us to load
in several programs at once, and switch between them at will, in a very clean fashion
(click on window beneath belonging to another program, select the program from the
Apple menu, or cycle through the program list on the upper right corner of the
MenuBar; this type of program switching is what tech note #180 calls “Major
Switching”). Additionally, programs that define themselves as ‘backgroundable’ can
get time when the ‘foreground’ program (the one who’s window is foremost and active)
is idle.
This is done by taking control when the foreground program calls
GetNextEvent(), EventAvail(), or WaitNextEvent(), a new call under multi finder, and
seeing what the foreground program’s event is. If the event is not a nullEvent, the
foreground program gets it. If the event is a nullEvent, then things get interesting and
somewhat confusing.
Null Event Processing
If our foreground program is not MultiFinder friendly, it’s doubtless using
GetNextEvent(). In this case, MultiFinder doesn’t do anything on the first nullEvent.
Or the second nullEvent (this is for programs that do work on nullEvents but were
written before the program’s authors knew about MultiFinder, like MS-Word). But
the third nullEvent the foreground program never sees (in the current version of
MultiFinder -- in later versions GNE() calling apps may only see 1 nullEvent, or
perhaps none). Instead MultiFinder looks about and sees if there are any background
tasks running (I’ll describe those in a moment), and turns control over to one of them
(this type of switching is called “Minor Switching” by tech note #180). That
background task then calls GNE() or WNE(), and MultiFinder takes over immediately,
checks the event, and if it’s not a nullEvent, returns control immediately to the
foreground program. If it is another nullEvent, MultiFinder checks to see if there are
any other background tasks, allocating nullEvents in a round-robin fashion until a
non-nullEvent comes along, at which time control is returned to the ‘foreground’ task,
which is returned that non-nullEvent.
For everything to work right, MultiFinder has to make certain assumptions.
First, it assumes that all programs call GNE(), WNE(), or EA() regularly (which a
program is under no obligation to do, though it would be nice...). If a program doesn’t,
then when it’s in the foreground, no background tasks will ever get time.
Furthermore, all programs that want to run as background tasks have to call one of
these three, and call them pretty often, in order to ensure that the foreground task can
quickly respond to any user event (a non-nullEvent, in this case). There’s no more a
way to stop a renegade background task from taking over the CPU the moment it gets it
than there is a way to stop a renegade program from making the machine crash --
programmers and users and the media will be the judge, not any hardware MMU.
So is it True™ multitasking? You decide.
How to be a (good) background task
Formally all you have to do to be a background task is to have a Size = -1
resource with the can background bit set. As a background task, to do any work, you
also have to check for, and use, nullEvents. Furthermore, the obedient background
task does no more than 50-100 milliseconds of work before calling GNE(), WNE(), or
EA() again. for a wonderful example of a disobedient background task, look at Apple’s
PrintMonitor that’s shipping with the 5.0 release (clearly following in MacWrite’s
lawless ways...). Caveat: in MultiFinder 1.0 there is a bug when using GNE() from
the background: you will get no nullEvents when you are first launched--you have to
be switched out and then back in again before you start seeing those precious
nullEvents.
As a background task, you can do almost anything a foreground task can do. You
can write to the screen, read from disk, use the memory manager, play sound, etc. You
can even be a foreground task (like a clock that updates the time constantly, and in the
foreground allows the user to change from 12 hour to 24 hour display--or like a
graphic load average program [which happens to be the subject of this article]).
However... you are warned to do all of your work inside of windows. Drawing to the
screen is a no-no. And if you are like a certain game I won’t mention (but who’s
initials are Crystal Quest), and you don’t draw in a window and expect the user to click
the mouse button whilst using your game, you will swiftly find yourself being switched
(twitched, as it’s sometimes called) out while whatever program’s window was
underneath the mouse when the mouseDown happened suddenly coming into the
foreground. Also you shouldn’t put up a Modal Dialog box, as it won’t come up in the
foreground but will stay in your layer and the user might never see it. Nor should you
do anything that might affect the foreground task, like fiddling with the menuBar or
cursor. Lastly, if your program is so uncouth as to change some system parameter
(like the mouse tracking ratio, as done by a certain program with the same initials),
you should at least be good enough to set it back to what it was when you get twitched
out.
Which brings us to the next level of MultiFinder compatibility: accepting
suspend/resume events (as the bit in the Size = -1 resource is labeled). These
programs recognize two new event types that came about from the Switcher days:
suspend and resume. A program (any program, not just backgroundable ones) that
accepts suspend events doesn’t ever get that first nullEvent (on a Major Switch),
(unless it’s got the background bit set, in which case it will share nullEvents with the
other programs that have a background bit set). Instead it will see a suspend event. A
suspend event is an app4Evt (theEvent.what = 15) with bit 0 of theEvent.message
clear. A resume event is an app4Evt with bit 0 set to 1. A suspend event means that
the program is about to be juggled (for some reason the future tense is juggled, and
otherwise it’s called twitching), and the program should set the state of anything it
muddled with back to what it was before it muddled with it and so forth, and then call
GNE(), WNE() or EA(). Furthermore, if bit 1 of theEvent.message of the suspend
event is set, the application should convert it’s internal clipboard to the deskscrap (or
vice versa if the event is a resume event) so that other applications can use it. If that
bit is clear, then no scrap conversion is necessary.
Another bit in the Size = -1 resource is the Juggler Aware bit. If this bit is set,
the program uses WNE() instead of GNE() if MultiFinder is running (see the example
in my code or in tech note #158 [where I stole it from] for how to do this). WNE()
has two additional parameters that it gets passed: sleep (an unsigned long), and
mouseRgn (a RgnHandle). Sleep is the number of ticks that the application can go
without getting a nullEvent (for things like updating a blinking cursor or clock getting
a character from the modem or some such). mouseRgn is a RgnHandle within which the
mouse can move and the program needn’t be consulted as to it changing shape. An
example of this is a terminal emulator, who’s cursor is an I-beam when it’s over the
content region of the window, and an arrow when it’s over anything else. NIL cast to
the proper variable type can be passed for either of these parameters.
Sayeth Apple: you shouldn’t set the Juggler Aware bit unless you use WNE(), and
recognize suspend/resume events, and are in all ways cognizant of MultiFinder and
running with other foreground and background tasks. And what does this involve?
Background tasking programs (in fact, all programs) should now do a number of
things in order to better function in the MultiFinder environment. Programs should
not do any work on updateEvents (other than updating the window in question with calls
to BeginUpdate() and EndUpdate(), if nothing else). If a foreground program’s window
is moved about, it my reveal a portion of a window underneath it that wasn’t revealed
before. That program may all of a sudden decide that, on getting it’s updateEvent, that
it wants to get some more characters from the modem and display them on the screen
and such (like another program that I won’t mention who’s initials [really are] UW).
All programs should include a Size = -1 resource, in which, if nothing else, they give
a preferred memory partition size that they want to run in. If none is given, 384K is
assumed (though this can be edited from the Get Info box in the Finder). Programs that
were designed for the 128K Mac (in the 128K Mac days...) wind up taking gobs of RAM
for no good reason. All programs need to support Desk Accessories, as it’s through this
mechanism that Major Switching takes place. Lastly, to better utilize the MultiFinder
environment (that allows multiple programs to be auto-launched on startup),
programs should save their window positions and states so that the next time that they
are invoked the user doesn’t have to fuss with moving around all of their windows to
where he wants them. The kosher way to do this is with GlobalToLocal() on the upper
left and lower right positions of your window (or just the upper left if it’s not
resizable), and saving that information into the resource or data fork of another file
(not the one that the program is in). Then call MoveWindow(savedPoint) to position it
before making it visible. Alternately you could save a copy of the window resource into
a configuration file. Just keep in mind that reading and writing to the window record
itself a no-no (says Apple), ‘cause it may change in the future. And by writing to a
file other than the program file, you allow yourself to be shared on a network (such as
AppleShare or TOPS). Of course, if you don’t want to be shared, than this isn’t a
problem. Unless you are a commercial program with single-user licensing only, you
may as well allow yourself to be shared.
Graphic Load Average
When I first saw MultiFinder in action, I immediately thought that some means of
measuring the ‘load average’ of the CPU was needed (after all, most all True™
multitasking systems have this). Being as there was no real way to measure this in
the traditional sense (the number of processes the CPU is actively switching through),
I decided that instead it would measure the number of nullEvents it got over a period of
time, and graph it’s results (which is a more meaningful measure under MultiFinder).
Unfortunately someone beat me to it (actually, a lot of people beat me to it, but
one sticks out in particular). I have to give credit to Erik Kilk, who posted Waiting to
the Usenet some 72 hours before I had my program running. Clearly the only way I
could salvage my ego was to try to make my program better than his.
Graphic Load Average (GLA) has four load meters, each of which can be set to a
different sensitivity and update duration (and width). These settings are set via a
hierarchical menu. Additionally it uses WNE(), and will not run if MultiFinder isn’t
present (a dialog box comes up to nicely inform you of this). Also there is an option to
Compress Juggler RAM, which makes a call to the new juggler call MFMaxMem().
While I have yet to see this call actually do something, certainly having it there
somehow improves GLA (it also shows how to make the call from LightSpeed C v. 2.13,
which doesn’t yet support these new calls -- see the modified-multi finder.h include
file that I made, with the generous help of Ed Tecot of Apple, to use with LightSpeed C).
[Note: The current version of LS C is 2.15. Updates are available from Think, or if
purchased from MacTutor, from us. -Ed]
Graphic Load Average Controls
Each set of triplets controls three things: the width item, which is the width of
the displayed graphic bar in the window, the ceiling, which is the number of ticks
which will equal a full bar, and the time over which the null events will be averaged.
The ceiling parameter is the sensitivity setting, as to how many ticks give a full height
bar. The time parameter determines the number of ticks to wait before updating the
display. If we set this to five, then we wait five ticks before updating the display with
the average number received for that bar. We take the average, and divide it by the
ceiling to calculate the height of the bar to be displayed. The ceiling is converted into
pixels, divided by the number of ticks that equals a full ceiling to find the number of
pixels to display.
By using the four indicators, you can have four different sensitivity settings for
how many ticks would result in a full bar display. The maximum setting is a single
tick. A tick is a 60th of a second. On each null event, the time is compared, so the
average time between null events is being measured. The more bars you display, the
longer the time between null events, which means more load on the machine.
Other points of interest and note: putting up a window in a saved position is all
very nice, but what if the program is first run on a MacII, and then later is run on a
MacPlus. It is important to check that window position is within the bounds of the
window (better still, within the dragBounds that you use for MoveWindow()). I
cheated in GLA, and took the cheap way out when it came to AppleShare. I wrote the
configuration information into the same file as I execute from, which means that I